package model.micro
{
	import events.BattleEvent;
	import flash.events.EventDispatcher;
	import model.BattleReport;
	import model.macro.Fortress;
	import model.macro.RangedWeapon;
	import model.macro.Territory;
	import model.macro.Unit;
	import model.micro.projectiles.Projectile;
	
	/**
	 * The top level model structure for the micro game. A battle consists of
	 * an army attacking the fortress and an army of garrisoned troops
	 * defending it. Defending troops can be deployed to the field or stationed
	 * on the battlements as archers.
	 *
	 * The battle is over when either side retreats/surrenders or all the units
	 * from one army have been destroyed.
	 */
	public class Battle extends EventDispatcher
	{
		/** Team defending the castle */
		public static const DEFENSE_TEAM:int = 0;
		/** Team attacking the castle */
		public static const ATTACK_TEAM:int = 1;
		
		/** List of all macro units for the attacking team */
		public var attackers:Vector.<Unit>;
		/** List of all macro units for the defending team */
		public var defenders:Vector.<Unit>;
		/** List of defending units garrisoned in the castle */
		public var garrison:Vector.<Unit>;
		/** List of defending archer units in the castle */
		public var archers:Vector.<ArcherUnit>;
		/** List of attack units on the field */
		public var attack_field_units:Vector.<FieldUnit>;
		/** List of defense units on the field */
		public var defense_field_units:Vector.<FieldUnit>;
		/** All the projectiles currently in flight */
		public var projectiles:Vector.<Projectile>;
		/** The fortress in this terrority for the defending army */
		public var fortress:MicroFortress;
		/** The ground object */
		public var ground:Ground;
		/** The attacker closest to the fortress */
		private var first_attacker:FieldUnit;
		/** The x value of the left side of the attacker closest to the fortress */
		private var first_attacker_left:Number;
		/** The defender furthest from the fortress */
		private var first_defender:FieldUnit;
		/** The x value of the right side of the defender furthest from the fortress */
		private var first_defender_right:Number;
		/** The territory this battle is in */
		private var _defending_territory:Territory;
		/** The invading territory */ // Maybe be a vector in the future
		private var _invading_territoy:Territory;
		/** Winner of the battler */
		private var winner:int;
		
		
		/**
		 * Create a new battle with the given armies.
		 * @param	attackers
		 * @param	defenders
		 */
		public function Battle(attackers:Vector.<Unit>, defenders:Vector.<Unit>, macro_fortress:Fortress, battle_territory:Territory, invading_territory:Territory)
		{
			this.ground = new Ground();
			this.fortress = new MicroFortress(macro_fortress, ground.getBoundingBox().top);
			this.defenders = defenders;
			this.attackers = attackers.concat();
			_defending_territory = battle_territory;
			_invading_territoy = invading_territory;
			
			attack_field_units = new Vector.<FieldUnit>();
			defense_field_units = new Vector.<FieldUnit>();
			projectiles = new Vector.<Projectile>();
			garrison = new Vector.<Unit>();
			archers = new Vector.<ArcherUnit>();
			
			for each (var attacker:Unit in attackers)
			{
				if (attacker.weapon is RangedWeapon)
				{
					attack_field_units.push(new RangedFieldUnit(attacker, 300 + Math.floor(Math.random() * 200), this));
				}
				else
				{
					attack_field_units.push(new FieldUnit(attacker, 300 + Math.floor(Math.random() * 200), this));
				}
			}
			
			for each (var defender:Unit in defenders)
			{
				if (defender.weapon is RangedWeapon)
				{
					archers.push(new ArcherUnit(defender, fortress));
				}
				else
				{
					garrison.push(defender);
				}
			}
		}
		
		/**
		 * Add a projectile to the battle.
		 * @param	projectile
		 */
		public function addProjectile(projectile:Projectile):void
		{
			projectiles.push(projectile);
			dispatchEvent(new BattleEvent(BattleEvent.PROJECTILE_CREATED, this, projectile));
		}
		
		/**
		 * Deploy a unit from the garrison to the field.
		 * @param	unit	the unit to deploy. Throws an error if unit is not
		 * in the garrison.
		 */
		public function deployGarrisonedUnit(unit:Unit):FieldUnit
		{
			var i:int = garrison.indexOf(unit);
			if (i == -1)
			{
				throw new Error("unit not in garrison");
			}
			garrison.splice(i, 1);
			var deployed_unit:FieldUnit = new FieldUnit(unit, fortress.getBoundingBox().right, this);
			defense_field_units.push(deployed_unit);
			dispatchEvent(new BattleEvent(BattleEvent.GARRISON_DEPLOYED, this, deployed_unit));
			
			return deployed_unit;
		}
		
		/**
		 * Returns the attacking field unit closest to the fortress.
		 * @param	require_calculate	Whether to necessarily recalculate the value. Defaults to false.
		 * @return	The attacker closest to the fortress.
		 */
		public function getFirstAttacker(require_calculate:Boolean = false):FieldUnit
		{
			if (require_calculate || (first_attacker == null) || first_attacker.readyToRemove() || (first_attacker.getBoundingBox().left != first_attacker_left))
			{
				first_attacker = calculateFirstAttacker();
			}
			return first_attacker;
		}
		
		/**
		 * * Returns the defending field unit furthest from the fortress.
		 * @param	require_calculate	Whether to necessarily recalculate the value. Defaults to false.
		 * @return	The defender furthest from the fortress.
		 */
		public function getFirstDefender(require_calculate:Boolean = false):FieldUnit
		{
			if (require_calculate || (first_defender == null) || first_defender.readyToRemove() || (first_defender.getBoundingBox().right != first_defender_right))
			{
				first_defender = calculateFirstDefender();
			}
			return first_defender;
		}
		
		/**
		 * Calculates the first attacker. Use getFirstAttacker
		 * @return
		 */
		private function calculateFirstAttacker():FieldUnit
		{
			if (attack_field_units.length == 0)
			{
				return null;
			}
			
			var first:FieldUnit = attack_field_units[0];
			
			for each (var unit:FieldUnit in attack_field_units)
			{
				if (unit.getBoundingBox().left < first.getBoundingBox().left)
				{
					first = unit;
				}
			}
			return first;
		}
		
		/**
		 * Calculates the first defender. Use getFirstDefender
		 * @return
		 */
		private function calculateFirstDefender():FieldUnit
		{
			if (defense_field_units.length == 0)
			{
				return null;
			}
			
			var result:FieldUnit = defense_field_units[0];
			
			for each (var unit:FieldUnit in defense_field_units)
			{
				if (unit.getBoundingBox().right > result.getBoundingBox().right)
				{
					result = unit;
				}
			}
			return result;
		}
		
		/**
		 * Advance the battle one frame.
		 */
		public function update():void
		{
			first_attacker = calculateFirstAttacker();
			first_defender = calculateFirstDefender();
			
			// defender victory
			if (attack_field_units.length == 0)
			{
				winner = Battle.DEFENSE_TEAM;
				dispatchEvent(new BattleEvent(BattleEvent.BATTLE_ENDED, this, generateReport(winner)));
			}
			// attacker victory
			if (defense_field_units.length == 0 && fortress.getHP() <= 0 && garrison.length == 0 && archers.length == 0)
			{
				winner = Battle.ATTACK_TEAM;
				dispatchEvent(new BattleEvent(BattleEvent.BATTLE_ENDED, this, generateReport(winner)));
			}
			
			for (var i:int = attack_field_units.length - 1; i >= 0; i--)
			{
				attack_field_units[i].update();
				if (attack_field_units[i].readyToRemove())
				{
					dispatchEvent(new BattleEvent(BattleEvent.UNIT_KILLED, this, attack_field_units[i]));
					attack_field_units[i].macroUnit.dead = true;
					attack_field_units.splice(i, 1);
				}
			}
			for (i = defense_field_units.length - 1; i >= 0; i--)
			{
				defense_field_units[i].update();
				if (defense_field_units[i].readyToRemove())
				{
					dispatchEvent(new BattleEvent(BattleEvent.UNIT_KILLED, this, defense_field_units[i]));
					defense_field_units[i].macroUnit.dead = true;
					defense_field_units.splice(i, 1);
				}
			}
			for (i = archers.length - 1; i >= 0; i--)
			{
				archers[i].update(this);
			}
			for (i = projectiles.length - 1; i >= 0; i--)
			{
				projectiles[i].update(this);
				if (projectiles[i].readyToRemove())
				{
					projectiles.splice(i, 1);
				}
			}
		}
		
		private function generateReport(winner:int):BattleReport
		{
			return new BattleReport(ATTACK_TEAM, BattleReport.RETREAT_BY_DEFENDER, defending_territory, invading_territoy, attackers, defenders);
		}
		
		public function get defending_territory():Territory 
		{
			return _defending_territory;
		}
		
		public function get invading_territoy():Territory 
		{
			return _invading_territoy;
		}
	}
}